1   // Copyright 2007, 2008, 2009, 2010, 2011 The Apache Software Foundation
2   //
3   // Licensed under the Apache License, Version 2.0 (the "License");
4   // you may not use this file except in compliance with the License.
5   // You may obtain a copy of the License at
6   //
7   // http://www.apache.org/licenses/LICENSE-2.0
8   //
9   // Unless required by applicable law or agreed to in writing, software
10  // distributed under the License is distributed on an "AS IS" BASIS,
11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  // See the License for the specific language governing permissions and
13  // limitations under the License.
14  
15  package org.apache.tapestry5.internal.services;
16  
17  import java.lang.reflect.Method;
18  import java.util.Collections;
19  import java.util.Date;
20  import java.util.List;
21  import java.util.Map;
22  
23  import org.apache.tapestry5.Block;
24  import org.apache.tapestry5.PropertyConduit;
25  import org.apache.tapestry5.PropertyConduit2;
26  import org.apache.tapestry5.beaneditor.NonVisual;
27  import org.apache.tapestry5.beaneditor.Validate;
28  import org.apache.tapestry5.integration.app1.data.IntegerHolder;
29  import org.apache.tapestry5.internal.InternalPropertyConduit;
30  import org.apache.tapestry5.internal.bindings.PropBindingFactoryTest;
31  import org.apache.tapestry5.internal.test.InternalBaseTestCase;
32  import org.apache.tapestry5.internal.util.Holder;
33  import org.apache.tapestry5.internal.util.IntegerRange;
34  import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
35  import org.apache.tapestry5.services.PropertyConduitSource;
36  import org.testng.annotations.AfterClass;
37  import org.testng.annotations.BeforeClass;
38  import org.testng.annotations.Test;
39  
40  /**
41   * Most of the testing occurs inside {@link PropBindingFactoryTest} (due to
42   * historical reasons).
43   */
44  public class PropertyConduitSourceImplTest extends InternalBaseTestCase
45  {
46      private PropertyConduitSource source;
47  
48      @BeforeClass
49      public void setup()
50      {
51          source = getObject(PropertyConduitSource.class, null);
52      }
53  
54      @AfterClass
55      public void cleanup()
56      {
57          source = null;
58      }
59  
60      @Test
61      public void literal_conduits_have_invariant_annotation()
62      {
63          PropertyConduit pc = source.create(CompositeBean.class, "12345");
64  
65          Invariant annotation = pc.getAnnotation(Invariant.class);
66  
67          assertNotNull(annotation);
68  
69          assertSame(annotation.annotationType(), Invariant.class);
70      }
71  
72      @Test
73      public void range_variable_to()
74      {
75          PropertyConduit pc = source.create(IntegerHolder.class, "10..value");
76          IntegerHolder h = new IntegerHolder();
77  
78          h.setValue(5);
79  
80          IntegerRange ir = (IntegerRange) pc.get(h);
81  
82          assertEquals(ir, new IntegerRange(10, 5));
83      }
84  
85      @Test
86      public void range_variable_from()
87      {
88          PropertyConduit pc = source.create(IntegerHolder.class, "value..99");
89          IntegerHolder h = new IntegerHolder();
90  
91          h.setValue(72);
92  
93          IntegerRange ir = (IntegerRange) pc.get(h);
94  
95          assertEquals(ir, new IntegerRange(72, 99));
96      }
97  
98      @Test
99      public void literal_conduits_are_not_updateable()
100     {
101         PropertyConduit pc = source.create(CompositeBean.class, "12345");
102         CompositeBean bean = new CompositeBean();
103 
104         try
105         {
106             pc.set(bean, 42);
107             unreachable();
108         } catch (RuntimeException ex)
109         {
110             assertEquals(ex.getMessage(), "Literal values are not updateable.");
111         }
112     }
113 
114     @Test
115     public void this_literal_conduit_is_not_updateable()
116     {
117         PropertyConduit normal = source.create(CompositeBean.class, "this");
118         CompositeBean bean = new CompositeBean();
119 
120         try
121         {
122             normal.set(bean, 42);
123             unreachable();
124         } catch (RuntimeException ex)
125         {
126             assertEquals(ex.getMessage(), "Literal values are not updateable.");
127         }
128     }
129 
130     @Test
131     public void question_dot_operator_for_object_type()
132     {
133         InternalPropertyConduit normal = (InternalPropertyConduit) source.create(CompositeBean.class,
134                 "simple.firstName");
135         InternalPropertyConduit smart = (InternalPropertyConduit) source.create(CompositeBean.class,
136                 "simple?.firstName");
137 
138         CompositeBean bean = new CompositeBean();
139         bean.setSimple(null);
140 
141         assertEquals(normal.getPropertyName(), "firstName");
142         assertEquals(smart.getPropertyName(), "firstName");
143 
144         try
145         {
146             normal.get(bean);
147             unreachable();
148         } catch (NullPointerException ex)
149         {
150             // Expected.
151         }
152 
153         assertNull(smart.get(bean));
154 
155         try
156         {
157             normal.set(bean, "Howard");
158             unreachable();
159         } catch (NullPointerException ex)
160         {
161             // Expected.
162         }
163 
164         // This will be a no-op due to the null property in the expression
165 
166         smart.set(bean, "Howard");
167     }
168 
169     static class GenericBean {
170         public List<Date> dates;
171         public List<GenericBean> genericBeans;
172         
173         public List<Long> getLongs() {
174             return Collections.emptyList();
175         }
176         
177         public void setMap(Map<String, Integer> map) {
178         }
179     }
180     
181     @Test
182     public void generic_types_are_determined()
183     {
184         PropertyConduit2 datesConduit = (PropertyConduit2) source.create(GenericBean.class, "dates");
185         PropertyConduit2 longsConduit = (PropertyConduit2) source.create(GenericBean.class, "longs");
186         PropertyConduit2 nestedDatesConduit = (PropertyConduit2) source.create(GenericBean.class, "genericBeans.get(0).dates");
187         PropertyConduit2 mapConduit = (PropertyConduit2) source.create(GenericBean.class, "map");
188         assertEquals(datesConduit.getPropertyGenericType().toString(), "java.util.List<java.util.Date>");
189         assertEquals(longsConduit.getPropertyGenericType().toString(), "java.util.List<java.lang.Long>");
190         assertEquals(nestedDatesConduit.getPropertyGenericType().toString(), "java.util.List<java.util.Date>");
191         assertEquals(mapConduit.getPropertyGenericType().toString(), "java.util.Map<java.lang.String, java.lang.Integer>");
192     }
193     
194     @Test
195     public void method_names_are_matched_caselessly()
196     {
197         InternalPropertyConduit conduit = (InternalPropertyConduit) source.create(CompositeBean.class,
198                 "GETSIMPLE().firstName");
199 
200         assertEquals(conduit.getPropertyName(), "firstName");
201 
202         CompositeBean bean = new CompositeBean();
203         SimpleBean inner = new SimpleBean();
204         bean.setSimple(inner);
205 
206         conduit.set(bean, "Howard");
207 
208         assertEquals(inner.getFirstName(), "Howard");
209     }
210 
211     @Test
212     public void generics()
213     {
214         String string = "surprise";
215         StringHolder stringHolder = new StringHolder();
216         stringHolder.put(string);
217         StringHolderBean bean = new StringHolderBean();
218         bean.setValue(stringHolder);
219 
220         PropertyConduit conduit = source.create(StringHolderBean.class, "value.get()");
221 
222         assertSame(conduit.get(bean), string);
223 
224         assertSame(conduit.getPropertyType(), String.class);
225     }
226 
227     public static class One<A, B>
228     {
229         A a;
230         B b;
231 
232         public A getA()
233         {
234             return a;
235         }
236 
237         public void setA(A a)
238         {
239             this.a = a;
240         }
241 
242         public B getB()
243         {
244             return b;
245         }
246 
247         public void setB(B b)
248         {
249             this.b = b;
250         }
251     }
252 
253     public static class Two<B> extends One<String, B>
254     {
255         String s;
256         B b2;
257 
258         public String getS()
259         {
260             return s;
261         }
262 
263         public void setS(String s)
264         {
265             this.s = s;
266         }
267 
268         public B getB2()
269         {
270             return b2;
271         }
272 
273         public void setB2(B b2)
274         {
275             this.b2 = b2;
276         }
277     }
278 
279     public static class Three extends Two<Long>
280     {
281         Long x;
282 
283         public Long getX()
284         {
285             return x;
286         }
287 
288         public void setX(Long x)
289         {
290             this.x = x;
291         }
292     }
293 
294     public static class WithParameters<C, T>
295     {
296         private C type1Property; // method access
297         public C type1Field; // field access
298         private T type2Property; // method access
299         public T type2Field; // field access
300 
301         private T[] type2ArrayProperty;
302         public T[] type2ArrayField;
303 
304         public C getType1Property()
305         {
306             return type1Property;
307         }
308 
309         public void setType1Property(C type1Property)
310         {
311             this.type1Property = type1Property;
312         }
313 
314         public T getType2Property()
315         {
316             return type2Property;
317         }
318 
319         public void setType2Property(T type2Property)
320         {
321             this.type2Property = type2Property;
322         }
323 
324         public T[] getType2ArrayProperty()
325         {
326             return type2ArrayProperty;
327         }
328 
329         public void setType2ArrayProperty(T[] type2ArrayProperty)
330         {
331             this.type2ArrayProperty = type2ArrayProperty;
332         }
333     }
334 
335     public static class RealizedParameters extends WithParameters<Holder<SimpleBean>, Long>
336     {
337     }
338 
339     public static class WithGenericProperties
340     {
341         public Holder<SimpleBean> holder = new Holder<SimpleBean>();
342     }
343 
344     public static interface GenericInterface<A, B>
345     {
346         A genericA();
347 
348         B genericB();
349     }
350 
351     public static class WithRealizedGenericInterface implements GenericInterface<String, Long>
352     {
353         String a;
354         Long b;
355 
356         public String genericA()
357         {
358             return a;
359         }
360 
361         public Long genericB()
362         {
363             return b;
364         }
365     }
366 
367     @Test
368     public void generic_properties()
369     {
370         final WithGenericProperties bean = new WithGenericProperties();
371         final String first = "John";
372         final String last = "Doe";
373         final SimpleBean simple = new SimpleBean();
374         simple.setLastName(last);
375         simple.setAge(2);
376         simple.setFirstName(first);
377         bean.holder.put(simple);
378 
379         PropertyConduit conduit = source.create(WithGenericProperties.class, "holder.get().firstName");
380         assertSame(conduit.get(bean), first);
381     }
382 
383     @Test
384     public void generic_parameterized_base_with_properties()
385     {
386         final String first = "John";
387         final String last = "Doe";
388         final SimpleBean simple = new SimpleBean();
389         simple.setAge(2);
390         simple.setFirstName(first);
391         simple.setLastName(last);
392 
393         final RealizedParameters bean = new RealizedParameters();
394         final Holder<SimpleBean> holder = new Holder<SimpleBean>();
395         holder.put(simple);
396         bean.setType1Property(holder);
397         bean.setType2Property(1234L);
398         bean.type1Field = holder;
399         bean.type2Field = 5678L;
400         bean.type2ArrayField = new Long[]
401                 {123L, 456L};
402 
403         PropertyConduit conduit = source.create(RealizedParameters.class, "type1property.get().firstName");
404         assertSame(conduit.get(bean), first);
405         conduit.set(bean, "Change");
406         assertSame(conduit.get(bean), "Change");
407         conduit.set(bean, first);
408 
409         conduit = source.create(RealizedParameters.class, "type1field.get().firstName");
410         assertSame(conduit.get(bean), first);
411 
412         conduit = source.create(RealizedParameters.class, "type2field");
413         assertEquals(conduit.get(bean), bean.type2Field);
414 
415         conduit = source.create(RealizedParameters.class, "type2property");
416         assertEquals(conduit.get(bean), bean.getType2Property());
417 
418         conduit = source.create(RealizedParameters.class, "type2ArrayField");
419         assertEquals(conduit.get(bean), bean.type2ArrayField);
420 
421     }
422 
423     @Test
424     public void generic_interface()
425     {
426         final WithRealizedGenericInterface bean = new WithRealizedGenericInterface();
427         bean.a = "Hello";
428         bean.b = 12345L;
429 
430         PropertyConduit conduit = source.create(WithRealizedGenericInterface.class, "genericA()");
431         assertSame(conduit.get(bean), "Hello");
432         conduit = source.create(WithRealizedGenericInterface.class, "genericB()");
433         assertEquals(conduit.get(bean), 12345L);
434     }
435 
436     @Test
437     public void generic_nested()
438     {
439         Three bean = new Three();
440         bean.setA("hello");
441         bean.setB(123L);
442         bean.setB2(1235L);
443         bean.setX(54321L);
444 
445         PropertyConduit conduit = source.create(Three.class, "a");
446         assertSame(conduit.get(bean), "hello");
447     }
448 
449     @Test
450     public void null_root_object()
451     {
452         PropertyConduit conduit = source.create(StringHolderBean.class, "value.get()");
453 
454         try
455         {
456             conduit.get(null);
457             unreachable();
458         } catch (NullPointerException ex)
459         {
460             assertEquals(ex.getMessage(), "Root object of property expression 'value.get()' is null.");
461         }
462     }
463 
464     @Test
465     public void null_property_in_chain()
466     {
467         PropertyConduit conduit = source.create(CompositeBean.class, "simple.lastName");
468 
469         CompositeBean bean = new CompositeBean();
470         bean.setSimple(null);
471 
472         try
473         {
474             conduit.get(bean);
475             unreachable();
476         } catch (NullPointerException ex)
477         {
478             assertMessageContains(ex, "Property 'simple' (within property expression 'simple.lastName', of",
479                     ") is null.");
480         }
481     }
482 
483     @Test
484     public void last_term_may_be_null()
485     {
486         PropertyConduit conduit = source.create(CompositeBean.class, "simple.firstName");
487 
488         CompositeBean bean = new CompositeBean();
489 
490         bean.getSimple().setFirstName(null);
491 
492         assertNull(conduit.get(bean));
493     }
494 
495     @Test
496     public void field_annotations_are_visible()
497     {
498         PropertyConduit conduit = source.create(CompositeBean.class, "simple.firstName");
499 
500         Validate annotation = conduit.getAnnotation(Validate.class);
501 
502         assertNotNull(annotation);
503 
504         assertEquals(annotation.value(), "required");
505     }
506 
507     @Test
508     public void method_invocation_with_integer_arguments()
509     {
510         PropertyConduit conduit = source.create(EchoBean.class, "echoInt(storedInt, 3)");
511         EchoBean bean = new EchoBean();
512 
513         for (int i = 0; i < 10; i++)
514         {
515             bean.setStoredInt(i);
516             assertEquals(conduit.get(bean), new Integer(i * 3));
517         }
518     }
519 
520     @Test
521     public void method_invocation_with_double_argument()
522     {
523         PropertyConduit conduit = source.create(EchoBean.class, "echoDouble(storedDouble, 2.0)");
524         EchoBean bean = new EchoBean();
525 
526         double value = 22. / 7.;
527 
528         bean.setStoredDouble(value);
529 
530         assertEquals(conduit.get(bean), new Double(2. * value));
531     }
532 
533     @Test
534     public void method_invocation_with_string_argument()
535     {
536         PropertyConduit conduit = source.create(EchoBean.class, "echoString(storedString, 'B4', 'AFTER')");
537         EchoBean bean = new EchoBean();
538 
539         bean.setStoredString("Moe");
540 
541         assertEquals(conduit.get(bean), "B4 - Moe - AFTER");
542     }
543 
544     @Test
545     public void method_invocation_using_dereference()
546     {
547         PropertyConduit conduit = source.create(EchoBean.class, "echoString(storedString, stringSource.value, 'beta')");
548         EchoBean bean = new EchoBean();
549 
550         StringSource source = new StringSource("alpha");
551 
552         bean.setStringSource(source);
553         bean.setStoredString("Barney");
554 
555         assertEquals(conduit.get(bean), "alpha - Barney - beta");
556     }
557 
558     @Test
559     public void top_level_list()
560     {
561         PropertyConduit conduit = source.create(EchoBean.class, "[ 1, 2.0, storedString ]");
562         EchoBean bean = new EchoBean();
563 
564         bean.setStoredString("Lisa");
565 
566         List l = (List) conduit.get(bean);
567 
568         assertListsEquals(l, new Long(1), new Double(2.0), "Lisa");
569     }
570 
571     @Test
572     public void empty_list()
573     {
574         PropertyConduit conduit = source.create(EchoBean.class, "[  ]");
575         EchoBean bean = new EchoBean();
576 
577         bean.setStoredString("Lisa");
578 
579         List l = (List) conduit.get(bean);
580 
581         assertEquals(l.size(), 0);
582     }
583 
584     @Test
585     public void list_as_method_argument()
586     {
587         PropertyConduit conduit = source.create(EchoBean.class, "echoList([ 1, 2.0, storedString ])");
588         EchoBean bean = new EchoBean();
589 
590         bean.setStoredString("Bart");
591 
592         List l = (List) conduit.get(bean);
593 
594         assertListsEquals(l, new Long(1), new Double(2.0), "Bart");
595     }
596 
597     @Test
598     public void arrays_as_method_argument()
599     {
600         PropertyConduit conduit = source.create(EchoBean.class, "echoArray(storedArray)");
601         EchoBean bean = new EchoBean();
602 
603         bean.setStoredArray(new Number[][]
604                 {new Integer[]
605                         {1, 2}, new Double[]
606                         {3.0, 4.0}});
607 
608         Number[][] array = (Number[][]) conduit.get(bean);
609 
610         assertArraysEqual(array[0], 1, 2);
611         assertArraysEqual(array[1], 3.0, 4.0);
612     }
613 
614     @Test
615     public void top_level_map()
616     {
617         PropertyConduit conduit = source.create(EchoBean.class, "{'one': true, 'two': 2.0, stringSource.value: 3, 'four': storedString}");
618         EchoBean bean = new EchoBean();
619 
620         bean.setStoredString("four");
621         bean.setStringSource(new StringSource("three"));
622 
623         Map actual = (Map) conduit.get(bean);
624         assertEquals(actual.get("one"), true);
625         assertEquals(actual.get("two"), 2.0);
626         assertEquals(actual.get("three"), 3L);
627         assertEquals(actual.get("four"), "four");
628     }
629 
630     @Test
631     public void empty_map()
632     {
633         PropertyConduit conduit = source.create(EchoBean.class, "{ }");
634         EchoBean bean = new EchoBean();
635         Map m = (Map) conduit.get(bean);
636 
637         assertTrue(m.isEmpty());
638 
639     }
640 
641     @Test
642     public void map_as_method_argument()
643     {
644         PropertyConduit conduit = source.create(EchoBean.class, "echoMap({ 1: 'one', 2.0: 'two', storedString: stringSource.value })");
645         EchoBean bean = new EchoBean();
646 
647         bean.setStoredString("3");
648         bean.setStringSource(new StringSource("three"));
649 
650         Map m = (Map) conduit.get(bean);
651         assertEquals("one", m.get(1L));
652         assertEquals("two", m.get(2.0));
653         assertEquals("three", m.get("3"));
654 
655     }
656 
657     @Test
658     public void not_operator()
659     {
660         PropertyConduit conduit = source.create(IntegerHolder.class, "! value");
661         IntegerHolder holder = new IntegerHolder();
662 
663         assertEquals(conduit.get(holder), Boolean.TRUE);
664 
665         holder.setValue(99);
666 
667         assertEquals(conduit.get(holder), Boolean.FALSE);
668     }
669 
670     @Test
671     public void not_operator_in_subexpression()
672     {
673         PropertyConduit conduit = source.create(Switch.class, "label(! value)");
674 
675         Switch sw = new Switch();
676 
677         assertEquals(conduit.get(sw), "aye");
678 
679         sw.setValue(true);
680 
681         assertEquals(conduit.get(sw), "nay");
682     }
683 
684     /**
685      * TAP5-330
686      */
687     @Test
688     public void object_methods_can_be_invoked()
689     {
690         PropertyConduit conduit = source.create(Block.class, "toString()");
691 
692         Block b = new Block()
693         {
694             @Override
695             public String toString()
696             {
697                 return "Do You Grok Ze Block?";
698             }
699         };
700 
701         assertEquals(conduit.get(b), "Do You Grok Ze Block?");
702     }
703 
704     @Test
705     public void parse_error_in_property_expression()
706     {
707         try
708         {
709             source.create(IntegerHolder.class, "getValue(");
710             unreachable();
711         } catch (RuntimeException ex)
712         {
713             //note that addition of map support changed how this expression was parsed such that the error is now
714             //reported at character 8, (, rather than 0: getValue(.
715             assertEquals(ex.getMessage(),
716                     "Error parsing property expression 'getValue(': line 1:8 no viable alternative at input '('.");
717         }
718     }
719 
720     @Test
721     public void lexer_error_in_property_expression()
722     {
723         try
724         {
725             source.create(IntegerHolder.class, "fred #");
726             unreachable();
727         } catch (RuntimeException ex)
728         {
729             assertEquals(ex.getMessage(),
730                     "Error parsing property expression 'fred #': Unable to parse input at character position 6.");
731         }
732     }
733 
734     @Test
735     public void boolean_constant_as_method_parameter()
736     {
737         Bedrock bedrock = new Bedrock();
738 
739         PropertyConduit trueConduit = source.create(Bedrock.class, "toName(true)");
740         PropertyConduit falseConduit = source.create(Bedrock.class, "toName(false)");
741 
742         assertEquals(trueConduit.get(bedrock), "Fred");
743         assertEquals(falseConduit.get(bedrock), "Barney");
744     }
745 
746     /**
747      * TAP5-747
748      */
749     @Test
750     public void dereference_result_of_method_invocation()
751     {
752         ComplexObject co = new ComplexObject();
753         PropertyConduit pc = source.create(ComplexObject.class, "get(nestedIndex).name");
754 
755         assertEquals(pc.get(co), "zero");
756 
757         co.setNestedIndex(1);
758 
759         assertEquals(pc.get(co), "one");
760     }
761 
762     @Test
763     public void public_object_field()
764     {
765         PublicFieldBean bean = new PublicFieldBean();
766 
767         bean.stringField = "x";
768 
769         PropertyConduit pc = source.create(PublicFieldBean.class, "stringField");
770 
771         assertEquals(pc.get(bean), "x");
772 
773         pc.set(bean, "y");
774 
775         assertEquals(bean.stringField, "y");
776     }
777 
778     @Test
779     public void navigate_through_public_field()
780     {
781         PublicFieldBean bean = new PublicFieldBean();
782         PublicFieldBeanHolder holder = new PublicFieldBeanHolder(bean);
783 
784         bean.stringField = "x";
785 
786         PropertyConduit pc = source.create(PublicFieldBeanHolder.class, "bean.stringField");
787 
788         assertEquals(pc.get(holder), "x");
789 
790         pc.set(holder, "y");
791 
792         assertEquals(bean.stringField, "y");
793     }
794 
795     @Test
796     public void public_primitive_field()
797     {
798         PublicFieldBean bean = new PublicFieldBean();
799 
800         bean.intField = 99;
801 
802         // check out the case insensitiveness:
803 
804         PropertyConduit pc = source.create(PublicFieldBean.class, "IntField");
805 
806         assertEquals(pc.get(bean), new Integer(99));
807 
808         pc.set(bean, 37);
809 
810         assertEquals(bean.intField, 37);
811     }
812 
813     @Test
814     public void annotation_of_public_field()
815     {
816         PropertyConduit pc = source.create(PublicFieldBean.class, "StringField");
817 
818         assertNotNull(pc.getAnnotation(NonVisual.class));
819     }
820 
821     /**
822      * TAP5-1555
823      */
824     @Test
825     public void this_and_null_inside_array()
826     {
827         PropertyConduit pc = source.create(NonVisualBean.class, "[this, null]");
828 
829         Object bean = new NonVisualBean();
830 
831         List list = (List) pc.get(bean);
832 
833         assertEquals(list.size(), 2);
834         assertSame(list.get(0), bean);
835         assertNull(list.get(1));
836     }
837 
838     /**
839      * TAP5-1673
840      */
841     @Test
842     public void public_static_fields_are_accessible()
843     {
844         PropertyConduit pc = source.create(PublicStaticFieldBean.class, "value");
845 
846         assertSame(pc.get(null), PublicStaticFieldBean.VALUE);
847 
848         pc.set(null, "new-value");
849 
850         assertEquals(PublicStaticFieldBean.VALUE, "new-value");
851     }
852 
853     @Test
854     public void final_static_fields_are_read_only()
855     {
856         PropertyConduit pc = source.create(PublicStaticFieldBean.class, "read_only");
857 
858         try
859         {
860             pc.set(null, "new-value");
861             unreachable();
862         } catch (RuntimeException ex)
863         {
864             assertEquals(ex.getMessage(),
865                     "Expression 'read_only' for class org.apache.tapestry5.internal.services.PublicStaticFieldBean is read-only.");
866         }
867     }
868 
869     @Test
870     public void public_static_field_in_an_array()
871     {
872         PropertyConduit pc = source.create(PublicStaticFieldBean.class, "[read_only]");
873 
874         // Need to instantiate it, or exception "Root object of property expression is null"
875 
876         List<String> actual = (List<String>) pc.get(new PublicStaticFieldBean());
877 
878         assertListsEquals(actual, PublicStaticFieldBean.READ_ONLY);
879     }
880     
881     // TAP5-1493
882     @Test
883     public void covariant_property_return_type() {
884 
885         // example from Howard
886         try {
887             assertConduitPropertyType(Foo.class, "bar", Bar.class);
888         } catch (AssertionError e) {
889             List<Method> matches = CollectionFactory.newList();
890             for (Method method : Foo.class.getMethods()) {
891                 if (method.getName().equals("getBar")) {
892                     matches.add(method);
893                 }
894             }
895             fail(String.format("%s (possible candidates %s)", e.getMessage(), matches)); 
896         }
897         assertConduitPropertyType(AbstractFoo.class, "bar", AbstractBar.class);
898         
899         // example from Robert
900         assertConduitPropertyType(RobertMyClass.class, "foo.robertBarValue", int.class);
901         
902     }
903     
904     // TAP5-1493
905     public static abstract class AbstractBar
906     {
907     }
908 
909     public static class Bar extends AbstractBar
910     {
911     }
912 
913     public static abstract class AbstractFoo
914     {
915         public abstract AbstractBar getBar();
916     }
917 
918     public static class Foo extends AbstractFoo
919     {
920         Bar bar;
921 
922         public Bar getBar()
923         {
924             return bar;
925         }
926     }
927 
928     public static interface RobertFoo
929     {
930         int getRobertFooValue();
931     }
932 
933     public static interface RobertBar extends RobertFoo
934     {
935         int getRobertBarValue();
936     }
937 
938     public static interface RobertBaz
939     {
940         RobertFoo getFoo();
941     }
942 
943     public static interface RobertQux extends RobertBaz
944     {
945         RobertBar getFoo();
946     }
947 
948     public static class RobertAbstractClass implements RobertBaz
949     {
950         public RobertFoo getFoo()
951         {
952             return null;
953         }
954     }
955 
956     public static class RobertMyClass extends RobertAbstractClass implements RobertQux
957     {
958         public RobertBar getFoo()
959         {
960             return null;
961         }
962     }
963     
964     private void assertConduitPropertyType(Class<?> origin, String property, Class<?> expectedType) {
965         assertEquals(expectedType, source.create(origin, property).getPropertyType());
966     }
967 }